Skip to content

Conversation

@wonderwhy-er
Copy link
Owner

@wonderwhy-er wonderwhy-er commented Dec 1, 2025

User description

Problem

After commit cc89540 (v0.2.19+), Desktop Commander fails to work with certain MCP clients like langchain-mcp-adapters and in Docker environments. The client receives anyio.BrokenResourceError when trying to fetch tools.

Reported by @ever1022 in GitHub issue comments.

Root Cause

The MCP stdio protocol requires the client to send the first message. Any server output to stdout before the client's initialization request breaks the protocol handshake.

The issue was a timing problem in the initialization sequence:

BEFORE (broken):
1. Load config
2. Initialize feature flags  ← logger.* calls write to stdout here!
3. Create transport           ← Too late, damage done
4. Set global.mcpTransport

When featureFlagManager.initialize() ran, global.mcpTransport didn't exist yet, so logger.ts fell back to writing JSON-RPC notifications directly to stdout. Additionally, even when the transport existed, sendLog() didn't check isInitialized before writing.

Solution

  1. Move transport creation before config loading - so all logging gets properly buffered from the start

  2. Buffer sendLog() messages - when isInitialized is false, store in buffer for replay after MCP handshake

  3. Guard sendProgress/sendCustomNotification - silently drop when not initialized

AFTER (fixed):
1. Create transport           ← First thing!
2. Set global.mcpTransport    ← All logging now buffered
3. Load config
4. Initialize feature flags   ← Logs buffered, not written to stdout
5. MCP handshake completes
6. Buffered messages replayed

Testing

Created a test script that:

  1. Spawns Desktop Commander
  2. Waits 3 seconds without sending any message
  3. Verifies no stdout output occurred
  4. Sends proper MCP initialize request
  5. Confirms server responds correctly

Before fix: Server wrote JSON-RPC notifications to stdout before client message
After fix: No premature stdout output, server responds correctly

Files Changed

  • src/index.ts - Move transport creation before config/feature flags init
  • src/custom-stdio.ts - Add buffering to sendLog(), guard sendProgress/sendCustomNotification

CodeAnt-AI Description

Prevent MCP server from writing to stdout before client handshake

What Changed

  • Initialize the MCP transport before loading configuration so all startup logs are buffered instead of written directly to stdout
  • Buffer log notifications until the MCP connection is initialized, then replay them without violating the "client sends first message" requirement
  • Suppress progress and custom notifications until initialization completes so MCP clients no longer see protocol errors during startup

Impact

✅ Fewer MCP startup failures
✅ Reliable connections with langchain MCP adapters and Docker clients
✅ No stray server logs before MCP client handshake

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented loss and out-of-order delivery of logs, progress updates, and custom notifications during startup by buffering and gating them until the system is ready.
  • Chores

    • Moved transport initialization earlier so message infrastructure is available from process start.
    • Reduced noisy startup logs and made background refresh timer non-blocking so the process can exit cleanly.

✏️ Tip: You can customize this high-level summary in your review settings.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Dec 1, 2025

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds buffering and initialization guards to the MCP transport so logs, progress, and custom notifications are queued until MCP initialization; transport creation is moved earlier and exported globally; feature-flag background timer and async logs are silenced to avoid emitting during initialization.

Changes

Cohort / File(s) Summary
Message buffering & init guards
src/custom-stdio.ts
Adds buffering of sendLog (queueing level, data, timestamp) when not initialized; sendProgress and sendCustomNotification now early-return if not initialized. Buffered items are replayed chronologically on enableNotifications. Comments added documenting MCP protocol constraints.
Transport initialization & global export
src/index.ts
Creates FilteredStdioServerTransport earlier in startup and assigns it to global.mcpTransport before configuration loading; removes duplicate later transport initialization so transport is created once and available app-wide for early buffering.
Silent feature-flag background ops
src/utils/feature-flags.ts
Calls unref() on background refresh timer to allow process exit; removes/quietens early/async log statements in fetchFlags and saveToCache to avoid emitting logs during MCP initialization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review buffered-replay correctness and ordering in src/custom-stdio.ts (race conditions, edge cases when enable/disable interleave).
  • Validate that moving transport creation earlier and exporting global.mcpTransport does not create startup race conditions or circular imports in src/index.ts.
  • Confirm silent changes in src/utils/feature-flags.ts don't hide useful failures (ensure errors still surface appropriately).

Possibly related PRs

Suggested labels

size:XL

Poem

🐰 With twitchy nose and whiskers bright,

I buffered logs through morning light,
When MCP awoke and gates were torn,
I replayed each message, one by one,
Hooray — no log was left forlorn! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main fix: resolving an MCP stdio protocol violation that occurs during startup, with specific mention of Docker/langchain compatibility improvements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/feature-flags-stdio-issue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai bot added the size:S This PR changes 10-29 lines, ignoring generated files label Dec 1, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/index.ts (1)

14-17: Consider consolidating the dual buffering mechanism.

The code currently maintains two separate buffers:

  1. deferredMessages array in index.ts (lines 14-17)
  2. messageBuffer inside FilteredStdioServerTransport

Both are flushed in the oninitialized callback (lines 113-117). While this works correctly, you could simplify by using only the transport's internal buffering mechanism and calling transport.sendLog() directly instead of deferLog().

Example simplification:

-const deferredMessages: Array<{level: string, message: string}> = [];
-function deferLog(level: string, message: string) {
-    deferredMessages.push({level, message});
-}
-
 async function runServer() {
   try {
     ...
     try {
-        deferLog('info', 'Loading configuration...');
+        transport.sendLog('info', 'Loading configuration...');
         await configManager.loadConfig();
-        deferLog('info', 'Configuration loaded successfully');
+        transport.sendLog('info', 'Configuration loaded successfully');
         ...
     }
     ...
     server.oninitialized = () => {
       transport.enableNotifications();
-      
-      // Flush all deferred messages from both index.ts and server.ts
-      while (deferredMessages.length > 0) {
-        const msg = deferredMessages.shift()!;
-        transport.sendLog('info', msg.message);
-      }
       flushDeferredMessages();
       ...
     };

Also applies to: 50-63, 113-116

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3f5824 and 4ee1411.

📒 Files selected for processing (2)
  • src/custom-stdio.ts (3 hunks)
  • src/index.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-25T16:02:52.184Z
Learnt from: tillahoffmann
Repo: wonderwhy-er/DesktopCommanderMCP PR: 137
File: src/index.ts:160-162
Timestamp: 2025-05-25T16:02:52.184Z
Learning: In stateless MCP server implementations using the modelcontextprotocol/typescript-sdk, the correct pattern is to call both transport.close() and server.close() in the request close event handler. This is the official recommended approach according to the documentation for ensuring complete isolation between requests and preventing request ID collisions in concurrent scenarios.

Applied to files:

  • src/index.ts
📚 Learning: 2025-05-25T16:02:52.184Z
Learnt from: tillahoffmann
Repo: wonderwhy-er/DesktopCommanderMCP PR: 137
File: src/index.ts:160-162
Timestamp: 2025-05-25T16:02:52.184Z
Learning: In stateless MCP server implementations using the modelcontextprotocol/typescript-sdk, the pattern of calling both transport.close() and server.close() in the request close event handler is the official recommended approach according to the documentation at https://github.com/modelcontextprotocol/typescript-sdk.

Applied to files:

  • src/index.ts
🧬 Code graph analysis (1)
src/index.ts (1)
src/custom-stdio.ts (1)
  • FilteredStdioServerTransport (18-410)
🔇 Additional comments (4)
src/custom-stdio.ts (3)

280-297: LGTM! Buffering implementation correctly prevents protocol violation.

The buffering logic properly defers sendLog calls until initialization completes, ensuring MCP protocol compliance. The timestamp-based ordering guarantees chronological replay.


330-333: LGTM! Correctly drops transient progress notifications.

Progress notifications are appropriately dropped rather than buffered, as replaying stale progress values after initialization would be misleading.


366-369: LGTM! Correctly drops transient custom notifications.

Custom notifications are appropriately dropped rather than buffered, preventing protocol violations while avoiding the replay of potentially stale notification data.

src/index.ts (1)

42-47: Early transport creation correctly fixes protocol violation.

The transport creation before config loading ensures buffering is available from the start, correctly addressing the MCP protocol violation. Type safety is already properly handled with a global type declaration in src/types.ts.

*/
public sendProgress(token: string, value: number, total?: number) {
// Don't send progress before initialization - would break MCP protocol
if (!this.isInitialized) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The progress notification method currently ignores the disableNotifications flag, so clients explicitly configured to have notifications disabled (e.g., Cline/vscode/claude-dev) will still receive progress JSON-RPC messages, which contradicts the intent of configureForClient and can break those clients; you should also short‑circuit when notifications are disabled. [logic error]

Severity Level: Minor ⚠️

Suggested change
if (!this.isInitialized) {
// Also respect clients that have notifications disabled entirely
if (!this.isInitialized || this.disableNotifications) {
Why it matters? ⭐

This is a real behavioral bug, not cosmetic. The class has an explicit
disableNotifications flag set in configureForClient for Cline/vscode/claude-dev,
and both sendLog and sendLogNotification already short-circuit on that flag,
meaning "notifications disabled" is intended to apply globally. However,
sendProgress only checks isInitialized and still emits JSON-RPC progress
notifications even when disableNotifications is true, which contradicts the
stated behavior and can still break those same clients. Adding the
this.disableNotifications check aligns sendProgress with the rest of the
class' behavior and prevents unwanted notifications from being sent.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/custom-stdio.ts
**Line:** 331:331
**Comment:**
	*Logic Error: The progress notification method currently ignores the `disableNotifications` flag, so clients explicitly configured to have notifications disabled (e.g., Cline/vscode/claude-dev) will still receive progress JSON-RPC messages, which contradicts the intent of `configureForClient` and can break those clients; you should also short‑circuit when notifications are disabled.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

*/
public sendCustomNotification(method: string, params: any) {
// Don't send custom notifications before initialization - would break MCP protocol
if (!this.isInitialized) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The custom notification method does not check the disableNotifications flag, so even when a client has been configured to disable all notifications, custom JSON-RPC notifications will still be sent, leading to inconsistent behavior and potential protocol issues for those clients; this should also bail out when notifications are disabled. [logic error]

Severity Level: Minor ⚠️

Suggested change
if (!this.isInitialized) {
// Also respect clients that have notifications disabled entirely
if (!this.isInitialized || this.disableNotifications) {
Why it matters? ⭐

Same story here: configureForClient explicitly sets disableNotifications
for certain clients and logs "Notifications disabled", and both sendLog and
sendLogNotification honor that flag. sendCustomNotification only checks
isInitialized and will still push arbitrary JSON-RPC notifications over
stdout even when disableNotifications is true, which is inconsistent and
undermines the purpose of the flag. Adding a this.disableNotifications
short-circuit fixes a real logic inconsistency and prevents custom notifications
from leaking to clients that are supposed to receive none.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/custom-stdio.ts
**Line:** 367:367
**Comment:**
	*Logic Error: The custom notification method does not check the `disableNotifications` flag, so even when a client has been configured to disable all notifications, custom JSON-RPC notifications will still be sent, leading to inconsistent behavior and potential protocol issues for those clients; this should also bail out when notifications are disabled.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Dec 1, 2025

CodeAnt AI finished reviewing your PR.

The MCP protocol requires the client to send the first message. Writing
to stdout before the client's initialization request breaks the protocol
handshake, causing 'BrokenResourceError' in clients like langchain-mcp-adapters.

Root cause: Transport was created AFTER config/feature flags initialization,
so logger.* calls during startup wrote directly to stdout (bypassing buffering)
or called sendLog() which also wrote to stdout without checking isInitialized.

Additional issue: Async feature flag fetch operations would log messages
AFTER the client had already started closing, causing timing conflicts.

Fixes:
1. Move FilteredStdioServerTransport creation before config loading
2. Buffer sendLog() messages when isInitialized is false
3. Guard sendProgress/sendCustomNotification when not initialized
4. Add .unref() to feature flag refresh interval for clean process exit
5. Remove async logging from feature flag fetch/save operations

Tested with langchain-mcp-adapters - now passes where it previously failed.

Fixes #issue-reported-by-ever1022
@wonderwhy-er wonderwhy-er force-pushed the fix/feature-flags-stdio-issue branch from 4ee1411 to 6c5a467 Compare December 1, 2025 17:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/index.ts (1)

125-148: Remove redundant direct stdout write in error handler; logger already handles it.

The error handler at lines 125-148 calls logger.error() at line 135, which properly routes through global.mcpTransport.sendLog() when available. The subsequent direct process.stdout.write() at line 142 is redundant—it duplicates the logging already performed and violates the protocol by bypassing the sendLog() buffering mechanism. Since global.mcpTransport is initialized at line 47 before server.connect() executes, the logger will correctly handle the error notification. Remove lines 141-147 and let the existing logger.error() call handle all error notification.

♻️ Duplicate comments (2)
src/custom-stdio.ts (2)

330-333: disableNotifications flag is not checked here.

As noted in a previous review, this guard should also respect the disableNotifications flag for consistency with sendLog and sendLogNotification.

-    if (!this.isInitialized) {
+    if (!this.isInitialized || this.disableNotifications) {

366-369: disableNotifications flag is not checked here.

Same issue as sendProgress — should also check disableNotifications for consistency with other notification methods.

-    if (!this.isInitialized) {
+    if (!this.isInitialized || this.disableNotifications) {
🧹 Nitpick comments (1)
src/index.ts (1)

49-64: **Inconsistent indentation in config loading block.**The lines 49-64 have extra indentation (6 spaces instead of expected 4 spaces) compared to the surrounding code. This appears to be a formatting inconsistency that should be aligned with the rest of the function.

-      try {
-          deferLog('info', 'Loading configuration...');
-          await configManager.loadConfig();
-          deferLog('info', 'Configuration loaded successfully');
-          
-          // Initialize feature flags (non-blocking)
-          deferLog('info', 'Initializing feature flags...');
-          await featureFlagManager.initialize();
-      } catch (configError) {
-          deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
-          if (configError instanceof Error && configError.stack) {
-              deferLog('debug', `Stack trace: ${configError.stack}`);
-          }
-          deferLog('warning', 'Continuing with in-memory configuration only');
-          // Continue anyway - we'll use an in-memory config
-      }
+    try {
+      deferLog('info', 'Loading configuration...');
+      await configManager.loadConfig();
+      deferLog('info', 'Configuration loaded successfully');
+      
+      // Initialize feature flags (non-blocking)
+      deferLog('info', 'Initializing feature flags...');
+      await featureFlagManager.initialize();
+    } catch (configError) {
+      deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
+      if (configError instanceof Error && configError.stack) {
+        deferLog('debug', `Stack trace: ${configError.stack}`);
+      }
+      deferLog('warning', 'Continuing with in-memory configuration only');
+      // Continue anyway - we'll use an in-memory config
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee1411 and 6c5a467.

📒 Files selected for processing (3)
  • src/custom-stdio.ts (3 hunks)
  • src/index.ts (1 hunks)
  • src/utils/feature-flags.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-25T16:02:52.184Z
Learnt from: tillahoffmann
Repo: wonderwhy-er/DesktopCommanderMCP PR: 137
File: src/index.ts:160-162
Timestamp: 2025-05-25T16:02:52.184Z
Learning: In stateless MCP server implementations using the modelcontextprotocol/typescript-sdk, the correct pattern is to call both transport.close() and server.close() in the request close event handler. This is the official recommended approach according to the documentation for ensuring complete isolation between requests and preventing request ID collisions in concurrent scenarios.

Applied to files:

  • src/index.ts
📚 Learning: 2025-05-25T16:02:52.184Z
Learnt from: tillahoffmann
Repo: wonderwhy-er/DesktopCommanderMCP PR: 137
File: src/index.ts:160-162
Timestamp: 2025-05-25T16:02:52.184Z
Learning: In stateless MCP server implementations using the modelcontextprotocol/typescript-sdk, the pattern of calling both transport.close() and server.close() in the request close event handler is the official recommended approach according to the documentation at https://github.com/modelcontextprotocol/typescript-sdk.

Applied to files:

  • src/index.ts
🧬 Code graph analysis (1)
src/index.ts (1)
src/custom-stdio.ts (1)
  • FilteredStdioServerTransport (18-410)
🔇 Additional comments (5)
src/utils/feature-flags.ts (2)

49-51: Good use of unref() to prevent blocking process exit.

This ensures the periodic refresh timer doesn't keep the process alive after the MCP client disconnects. The explicit comment explaining why is helpful for future maintainers.


114-114: Silent logging approach is appropriate for async operations.

Removing log statements from async code paths (fetch and cache save) that may execute after MCP client disconnection is the right call. These operations now fail silently, relying on the debug-level error logging in the catch blocks instead.

Also applies to: 139-141, 160-160

src/custom-stdio.ts (1)

288-297: Buffering logic correctly mirrors the console redirection pattern.

The implementation properly buffers messages with level, structured args, and timestamp, matching the existing setupConsoleRedirection pattern. This ensures all sendLog calls during startup are captured and replayed after MCP initialization.

src/index.ts (2)

42-47: Correct fix: transport creation moved before config/feature-flag initialization.

This ensures global.mcpTransport is set before any code path can trigger logger.* calls, allowing the FilteredStdioServerTransport to buffer all messages properly. The explicit comment documents the ordering constraint for future maintainers.


107-122: Well-structured event-driven initialization completion.

The use of server.oninitialized callback correctly defers notification enabling and message flushing until the MCP handshake is complete. This follows the learnings about proper MCP protocol handling with the TypeScript SDK.

@wonderwhy-er wonderwhy-er merged commit 36caa10 into main Dec 2, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants